/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2009 - 2012 Luca Mingardi.
 *
 * This file is part of jeeObserver.
 *
 * JeeObserver is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * JeeObserver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package jeeobserver.server.database;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import jeeobserver.JvmCoreElement;
import jeeobserver.server.JeeObserverServerContext;
import jeeobserver.server.JvmParameters;
import jeeobserver.server.JvmParameters.SearchElementsParameter;
import jeeobserver.server.JvmStatistics;
import jeeobserver.server.database.DatabaseFile.DatabaseEntity;
import jeeobserver.server.database.DatabaseFileElements.DatabaseElementEntity;
import jeeobserver.server.database.DatabaseFileStatistics.DatabaseStatisticsEntity;
import jeeobserver.server.JvmStatistics.Element;
import jeeobserver.server.JvmStatistics.ElementStatistics;
import jeeobserver.server.JvmStatistics.JvmTimeStatistics;
import jeeobserver.server.JvmStatistics.JvmTotalStatistics;
import jeeobserver.server.RequestParameter;
import jeeobserver.server.Statistics.DateStatistics;
import jeeobserver.server.database.DatabaseFileStatistics.StatisticsHeaderEntity;
import jeeobserver.utilities.DatabaseUtilities;

class FileJvmHandler {

    private static final String ELEMENTS_FILE_NAME = "jvm_elements";

    private static final String STATISTICS_FILE_NAME = "jvm_statistics";

    private String path;

    private int samplingPeriod;

    private DatabaseFileElements<JvmElementEntity> fileElements;

    private DatabaseFileStatistics fileStatistics;

    //Chached indexes
    public FileJvmHandler(String path, int samplingPeriod) throws IOException {

        this.path = path;

        this.samplingPeriod = samplingPeriod;

        //Create files (if not exists)
        this.fileElements = new DatabaseFileElements<JvmElementEntity>(this.path + ELEMENTS_FILE_NAME, JvmElementEntity.class);

        this.fileStatistics = new DatabaseFileStatistics(this.path + STATISTICS_FILE_NAME, JvmStatisticsEntity.class, JvmStatisticsEntity.BYTES_SIZE);

        StatisticsHeaderEntity headerEntity = this.fileStatistics.getHeader();

        if (headerEntity.getSamplingPeriod() != this.samplingPeriod) {
            this.fileElements.clear();
            this.fileStatistics.clear();

            headerEntity.setSamplingPeriod((byte) this.samplingPeriod);
            headerEntity.setUnoptimizedRows(0);

            this.fileStatistics.updateHeader(headerEntity);
        }

    }

    public synchronized void persistStatistics(List<JvmCoreElement> invocationsList) throws IOException {

        if (invocationsList.isEmpty()) {
            return;
        }

        JeeObserverServerContext.logger.log(Level.FINER, "Persisting {0} Jvm statistics.", String.valueOf(invocationsList.size()));

        Map<Integer, DatabaseStatisticsEntity> statisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();

        Map<Integer, DatabaseElementEntity> elementsMap = new HashMap<Integer, DatabaseElementEntity>();


        //Iterate and group all invocations
        for (final JvmCoreElement coreElement : invocationsList) {

            //Find statistics sampling date
            final Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(coreElement.getTimestamp(), this.samplingPeriod);

            //Insert into cache all elements
            JvmElementEntity elementEntity = new JvmElementEntity(coreElement.getContext());

            if (!elementsMap.containsKey(elementEntity.getElement().getId())) {
                elementsMap.put(elementEntity.getElement().getId(), elementEntity);
            }

            //Insert into cache all statistics
            DatabaseStatisticsEntity statisticsEntity = new JvmStatisticsEntity(elementEntity.getElement().getId(), samplingDate, coreElement);

            if (!statisticsMap.containsKey(statisticsEntity.getId())) {
                statisticsMap.put(statisticsEntity.getId(), statisticsEntity);
            } else {
                statisticsMap.get(statisticsEntity.getId()).add(statisticsEntity);
            }
        }


        //Persist cached elements into Database
        this.fileElements.merge(elementsMap);


        //Persist cached statistics into Database
        this.fileStatistics.persist(new HashSet(statisticsMap.values()));



    }

    public synchronized ResultSetElement<JvmStatistics.Element> searchElements(JvmParameters.SearchElementsParameter parameters) throws IOException {

        JeeObserverServerContext.logger.log(Level.FINE, "Searching Jvm elements. {0}", parameters.toString());

        final ResultSetElement<JvmStatistics.Element> resultList = new ResultSetElement<JvmStatistics.Element>();

        this.fileElements.reset();

        JvmElementEntity elementEntity;

        while ((elementEntity = (JvmElementEntity) this.fileElements.read()) != null) {
            if (elementEntity.passFilter(parameters.getElementId(), parameters.getContext())) {
                resultList.add(elementEntity.getElement());
            }
        }

        return resultList;
    }

    public synchronized ResultSetStatistics<JvmStatistics> searchStatistics(JvmParameters.SearchStatisticsParameter parameters) throws IOException {

        JeeObserverServerContext.logger.log(Level.FINE, "Searching Jvm statistics. {0}", parameters.toString());

        //Check groupBy parameter
        if (parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT) {
            throw new IOException("GroupBy: " + parameters.getGroupBy() + " parameter not valid.");
        }

        //Search elements
        ResultSetElement<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getElementId(), parameters.getContext()));

        //Group by
        Map<Integer, Element> elementsGroupedElements = new HashMap();
        Map<Integer, Integer> elementsGroupedIds = new HashMap();

        for (Element element : elements) {

            int elementGroupedId = 0;

            if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
                elementGroupedId = element.getId();
            }

            if (!elementsGroupedIds.containsKey(element.getId())) {
                elementsGroupedIds.put(element.getId(), elementGroupedId);
            }

            if (!elementsGroupedElements.containsKey(elementGroupedId)) {
                elementsGroupedElements.put(elementGroupedId, element);
            }
        }


        Map<Date, Map<Integer, JvmStatistics>> statisticsPerPeriodMap = new HashMap();

        long startTime = System.currentTimeMillis();

        List<JvmStatisticsEntity> statisticsList = this.fileStatistics.search(elementsGroupedIds.keySet(), parameters.getDateFrom(), parameters.getDateTo());

        for (JvmStatisticsEntity statisticEntity : statisticsList) {

            //System.out.println("JVM ENTITY: " + statisticEntity.getDate() + " - " + statisticEntity.getCount() + " --> " + statisticEntity.getCpuNanosStatistics().getSum());

            //Get grouped id
            int elementGroupedId = elementsGroupedIds.get(statisticEntity.getElementId());

            //Get statistics sampling period
            Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), parameters.getSamplingPeriod());

            JvmStatistics statistics = null;

            Element element = elementsGroupedElements.get(elementGroupedId);

            if (parameters.getSamplingPeriod() == RequestParameter.SAMPLING_PERIOD_NONE) {
                if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
                    statistics = new JvmTotalStatistics(statisticEntity, element.getContext());
                }
            } else {
                statistics = new JvmTimeStatistics(statisticEntity);
            }

            if (!statisticsPerPeriodMap.containsKey(samplingDate)) {
                statisticsPerPeriodMap.put(samplingDate, new HashMap<Integer, JvmStatistics>());
            }

            if (statisticsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
                statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId).add(statistics);
            } else {
                statisticsPerPeriodMap.get(samplingDate).put(elementGroupedId, statistics);
            }
        }

        long stopTime = System.currentTimeMillis();

        System.out.println("Time: " + (stopTime - startTime));

        final ResultSetStatistics<JvmStatistics> resultList = new ResultSetStatistics<JvmStatistics>(this.samplingPeriod);

        for (Date date : statisticsPerPeriodMap.keySet()) {
            Map<Integer, JvmStatistics> statisticsMap = statisticsPerPeriodMap.get(date);

            for (int id : statisticsMap.keySet()) {
                JvmStatistics statistics = statisticsMap.get(id);

                //statistics.calculateTrendStatistics();

                resultList.add(statistics);
            }
        }

        return resultList;
    }

    public synchronized void deleteStatistics(JvmParameters.DeleteStatisticsParameter parameters) throws IOException {

        JeeObserverServerContext.logger.log(Level.FINE, "Deleting Jvm statistics. {0}", parameters.toString());

        //Filter elements
        ResultSetElement<JvmStatistics.Element> elements = searchElements(new JvmParameters.SearchElementsParameter(parameters.getElementId(), parameters.getContext()));

        Set<Integer> elementsSet = new HashSet<Integer>();

        for (JvmStatistics.Element element : elements) {
            elementsSet.add(element.getId());
        }



        //Delete statistics
        this.fileStatistics.delete(elementsSet, parameters.getDateFrom(), parameters.getDateTo());


        Set<Integer> elementsToNotDeleteSet = new HashSet<Integer>();

        //Delete elements
        this.fileElements.reset();

        JvmElementEntity elementEntity;

        while ((elementEntity = (JvmElementEntity) this.fileElements.read()) != null) {

            if (elementEntity.getStatus() != DatabaseElementEntity.STATUS_DELETED) {
                if (!elementsToNotDeleteSet.contains(elementEntity.getElement().getId())) {
                    this.fileElements.writePervious(elementEntity);
                }
            }
        }
    }

    public synchronized void close() throws IOException {
        this.fileStatistics.close();
        this.fileElements.close();
    }

    static class JvmElementEntity extends DatabaseElementEntity implements DatabaseEntity {

        private static final long serialVersionUID = 40L;

        private Element element;

        public JvmElementEntity(byte[] bytes) {
            ByteBuffer buffer = ByteBuffer.wrap(bytes);

            this.status = buffer.get();

            this.element = new Element(buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()));
        }

        public JvmElementEntity(String context) {
            this.element = new Element(context);
        }

        public byte[] getBytes() {
            ByteBuffer buffer = ByteBuffer.allocate(this.getSize());

            buffer.put(this.getStatus());

            buffer.putInt(this.getElement().getId());

            buffer.putInt(this.getElement().getContext().getBytes().length);
            buffer.put(this.getElement().getContext().getBytes());

            return buffer.array();
        }

        public int getSize() {
            return (13 + element.getContext().getBytes().length);
        }

        public Element getElement() {
            return element;
        }

        public boolean passFilter(Integer id, String context) {

            if (this.status == DatabaseElementEntity.STATUS_DELETED) {
                return false;
            } else if (id != null && id.intValue() != this.element.getId()) {
                return false;
            } else if (context != null && !context.equals(this.element.getContext())) {
                return false;
            }

            return true;
        }
    }

    static class JvmStatisticsEntity extends ElementStatistics implements DatabaseStatisticsEntity {

        private static final long serialVersionUID = 40L;

        public static final int BYTES_SIZE = 373;

        private transient byte status;

        public JvmStatisticsEntity(ByteBuffer buffer) {
            this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.slice());
        }

        public JvmStatisticsEntity(byte[] bytes) {
            this(ByteBuffer.wrap(bytes));
        }

        public JvmStatisticsEntity(byte status, int elementId, Date date, int count, ByteBuffer buffer) {

            super(elementId, date, count);

            this.status = status;

            this.dateStatistics = new DateStatistics(new Date(buffer.getLong()), new Date(buffer.getLong()));

            this.cpuNanosStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.cpuPercentageStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);

            this.heapUsedBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.heapCommittedBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.heapTotalBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);

            this.nonHeapUsedBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.nonHeapCommittedBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.nonHeapTotalBytesStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);

            this.threadsCountStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
            this.processorsCountStatistics = FileDatabaseHandler.getNumberStatistics(buffer, this.date);
        }

        public JvmStatisticsEntity(int elementId, Date date, JvmCoreElement coreElement) {
            super(elementId, date, 1);

            this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;

            this.dateStatistics = new DateStatistics(coreElement.getTimestamp());

            this.cpuNanosStatistics = new NumberStatistics(this.date, coreElement.getCpuNanos());

            this.cpuPercentageStatistics = new NumberStatistics(this.date, (coreElement.getCpuNanos() / (coreElement.getTimeNanos() * coreElement.getProcessorsCount())));

            this.heapUsedBytesStatistics = new NumberStatistics(this.date, coreElement.getHeapUsedBytes());
            this.heapCommittedBytesStatistics = new NumberStatistics(this.date, coreElement.getHeapCommittedBytes());
            this.heapTotalBytesStatistics = new NumberStatistics(this.date, coreElement.getHeapTotalBytes());

            this.nonHeapUsedBytesStatistics = new NumberStatistics(this.date, coreElement.getNonHeapUsedBytes());
            this.nonHeapCommittedBytesStatistics = new NumberStatistics(this.date, coreElement.getNonHeapCommittedBytes());
            this.nonHeapTotalBytesStatistics = new NumberStatistics(this.date, coreElement.getNonHeapTotalBytes());

            this.threadsCountStatistics = new NumberStatistics(this.date, coreElement.getThreadsCount());
            this.processorsCountStatistics = new NumberStatistics(this.date, coreElement.getProcessorsCount());
        }

        public byte[] getBytes() {

            ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

            buffer.put(this.status);

            buffer.putInt(this.elementId);

            FileDatabaseHandler.fillBuffer(buffer, this);

            FileDatabaseHandler.fillBuffer(buffer, this.dateStatistics);

            FileDatabaseHandler.fillBuffer(buffer, this.cpuNanosStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.cpuPercentageStatistics);

            FileDatabaseHandler.fillBuffer(buffer, this.heapUsedBytesStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.heapCommittedBytesStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.heapTotalBytesStatistics);

            FileDatabaseHandler.fillBuffer(buffer, this.nonHeapUsedBytesStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.nonHeapCommittedBytesStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.nonHeapTotalBytesStatistics);

            FileDatabaseHandler.fillBuffer(buffer, this.threadsCountStatistics);
            FileDatabaseHandler.fillBuffer(buffer, this.processorsCountStatistics);

            return buffer.array();
        }

        public int getSize() {
            return BYTES_SIZE;
        }

        public byte getStatus() {
            return status;
        }

        public void setStatus(byte status) {
            this.status = status;
        }

        public void add(DatabaseStatisticsEntity entity) {
            super.add((JvmStatisticsEntity) entity);
        }
    }
}
